using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.ComponentModel;
using System.Windows.Forms;
using System.Text.RegularExpressions;

namespace ScreenMonkey.Plugin.Clips.Song
{
    [Flags]
    public enum SearchOptionFlags
    {
        None = 0,
        All = 1,
        Title = 2,
        Author = 4,
        Reference = 8,
        Body = 16
    }

    public class LyricsSearch: IDisposable
    {
        BackgroundWorker searchWorker = new BackgroundWorker();
        BackgroundWorker loadCacheWorker = new BackgroundWorker();
        BackgroundWorker processLabelsWorker = new BackgroundWorker();

        private Dictionary<string, SongLyrics> songCache = new Dictionary<string,SongLyrics>();
        private List<SongLyrics> searchCache = new List<SongLyrics>();

        private FileSystemWatcher watch = new FileSystemWatcher();

        object resultsLock = new object();

        public event EventHandler StartSearching;
        public event EventHandler FinishedSearching;
        public event EventHandler<SearchResultArgs> NewResult;
        public event EventHandler LoadProgressChanged;

        public LyricsSearch()
        {
            loadCacheWorker.WorkerReportsProgress = true;

            loadCacheWorker.DoWork += new DoWorkEventHandler(loadCacheWorker_DoWork);
            loadCacheWorker.ProgressChanged += new ProgressChangedEventHandler(loadCacheWorker_ProgressChanged);
            processLabelsWorker.DoWork += new DoWorkEventHandler(processLabelsWorker_DoWork);

            searchWorker.DoWork += new DoWorkEventHandler(searchWorker_DoWork);
            searchWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(searchWorker_RunWorkerCompleted);

            searchLabels.ListChanged += new ListChangedEventHandler(searchLabels_ListChanged);

            watch.Changed += new FileSystemEventHandler(watch_Changed);
            watch.Created += new FileSystemEventHandler(watch_Created);
            watch.Deleted += new FileSystemEventHandler(watch_Deleted);
            watch.Renamed += new RenamedEventHandler(watch_Renamed);
            watch.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
            watch.Filter = "*.xml";            
        }

        void loadCacheWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            LoadProgress = e.ProgressPercentage;
        }

        private SearchOptionFlags searchFields = SearchOptionFlags.All;

        public SearchOptionFlags SearchFields
        {
            get { return searchFields; }
            set { searchFields = value; }
        }

        private int loadProgress = 0;

        public int LoadProgress
        {
            get { return loadProgress; }
            protected set 
            {
                if (loadProgress != value)
                {
                    loadProgress = value;
                    if (LoadProgressChanged != null)
                        LoadProgressChanged(this, EventArgs.Empty);
                }                
            }
        }

        void watch_Renamed(object sender, RenamedEventArgs e)
        {
            //An item in our song library has been renamed.
            lock (songCache)
            {
                SongLyrics item;
                if (songCache.TryGetValue(e.OldFullPath, out item))
                {
                    songCache.Remove(e.OldFullPath);
                    item.FileName = e.FullPath;
                    songCache.Add(e.FullPath,item);
                }
            }
        }

        void watch_Deleted(object sender, FileSystemEventArgs e)
        {
            //A file has been deleted from our song library.
            lock (songCache) songCache.Remove(e.FullPath);
            ProcessLabels();
        }

        void watch_Created(object sender, FileSystemEventArgs e)
        {
            //A new file has been added to our song library.
            CacheSong(e.FullPath, true);
            ProcessLabels();
        }

        void watch_Changed(object sender, FileSystemEventArgs e)
        {
            //One of the files in the search directory has changed.
            CacheSong(e.FullPath,true);
        }

        void searchLabels_ListChanged(object sender, ListChangedEventArgs e)
        {
            InvalidateSearch();
            ProcessLabels();
        }

        void loadCacheWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            lock (songCache) songCache.Clear();

            if (Directory.Exists(DirectoryPath))
            {
                DirectoryInfo songFolder = new DirectoryInfo(DirectoryPath);

                loadCacheWorker.ReportProgress(0);

                FileInfo[] songFiles = songFolder.GetFiles("*.xml");
                int index = 0;
                foreach (FileInfo file in songFiles)
                {                    
                    CacheSong(file.FullName, false);
                    loadCacheWorker.ReportProgress((index * 100) / songFiles.Length);
                    index++;                    
                }
            }

            InvalidateSearch();
            ProcessLabels();
            Search(SearchText);

            loadCacheWorker.ReportProgress(100);
        }

        void processLabelsWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            ProcessLabels();
        }

        void searchWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            OnFinishedSearch();

            if (PendingSearch.Length > 2)
            {
                Search(PendingSearch);
                PendingSearch = "";
            }
        }

        public void ParseSearchTerm(string term, out string searchTerm, out SearchOptionFlags option)
        {
            option = SearchOptionFlags.None;

            //Look for tokens in the search term.
            string[] termParts = term.Split(':');
            if (termParts.Length > 1)
            {
                switch (termParts[0].ToLower())
                {
                    case "all":
                        option = SearchOptionFlags.All;
                        break;
                    case "author":
                        option = SearchOptionFlags.Author;
                        break;
                    case "body":
                        option = SearchOptionFlags.Body;
                        break;
                    case "title":
                        option = SearchOptionFlags.Title;
                        break;
                    case "reference":
                        option = SearchOptionFlags.Reference;
                        break;
                }

                searchTerm = FilterText(termParts[1]);
            }
            else
                searchTerm = FilterText(term);
        }

        void searchWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            if (DirectoryPath != null)
            {
                SearchOptionFlags searchOptions;
                string searchTerm;

                //Get the search text and any options from the entered text.
                ParseSearchTerm((string)e.Argument, out searchTerm, out searchOptions);
                if (searchOptions == SearchOptionFlags.None)
                    searchOptions = SearchFields;

                lock (searchLabels)
                {
                    //Only perform a text search if the search terms length is greater than 2 characters.
                    if (searchTerm.Length > 2)
                    {
                        if (SearchText.Trim() != "" && searchTerm.Contains(SearchText) && !invalidateSearch)
                        {
                            List<SongLyrics> oldResults = new List<SongLyrics>(searchResults);

                            SearchText = searchTerm;
                            searchResults.Clear();

                            foreach (SongLyrics item in oldResults)
                            {
                                if (FindInSong(item, searchTerm, searchOptions))
                                {
                                    searchResults.Add(item);
                                    OnNewResult(item.FileName, item);
                                }
                            }
                        }
                        else
                        {
                            SearchText = searchTerm;
                            searchResults.Clear();

                            lock (searchCache)
                            {
                                foreach (SongLyrics song in searchCache)
                                {
                                    if (FindInSong(song, searchTerm, searchOptions))
                                    {
                                        searchResults.Add(song);
                                        OnNewResult(song.FileName, song);
                                    }
                                }
                            }

                        }
                    }
                    else
                    {
                        //When the search term is empty provide all songs as the result.
                        searchResults.Clear();

                        //We need to add all the items to the search result but we have to do them
                        //individually so that the OnNewResult event is fired for each song.
                        foreach (SongLyrics song in searchCache)
                        {
                            //Add this sond to the search results.
                            searchResults.Add(song);
                            OnNewResult(song.FileName, song);
                        }
                    }
                }
            }
        }

        private bool invalidateSearch = false;
        public void InvalidateSearch()
        {
            invalidateSearch = true;
        }

        protected string FilterText(string text)
        {
            // replace each series of punctuation and white space with a single space character
            return Regex.Replace(text, "[\\s\\p{P}]+", " ");
        }

        private void ProcessLabels()
        {
            lock (searchCache)
            {
                searchCache.Clear();

                lock (songCache)
                {
                    foreach (SongLyrics song in songCache.Values)
                    {
                        if (SearchLabels.Count == 0)
                        {
                            searchCache.Add(song);
                        }
                        else
                        {
                            foreach (string label in SearchLabels)
                            {
                                if (song.MatchLabel(label))
                                {
                                    searchCache.Add(song);
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }

        protected void OnNewResult(string filename, SongLyrics lyrics)
        {
            if (NewResult != null) NewResult(this, new SearchResultArgs(filename,lyrics));
        }

        protected void OnStartSearch()
        {
            if (StartSearching != null) StartSearching(this, EventArgs.Empty);
        }

        protected void OnFinishedSearch()
        {
            if (FinishedSearching != null) FinishedSearching(this, EventArgs.Empty);
        }

        public bool HasSearchFlags(string term)
        {
            SearchOptionFlags options;
            ParseSearchTerm(term, out term, out options);
            return options != SearchOptionFlags.None;
        }

        private bool FindInSong(SongLyrics song, string term, SearchOptionFlags searchOptions)
        {
            string searchText = string.Empty;

            //Determine the type of search to perform.
            if ((searchOptions & SearchOptionFlags.All)>0)
            {
                searchText = FilterText(song.OriginalXml);
            }
            else
            {
                if ((searchOptions & SearchOptionFlags.Author) > 0)
                    searchText += FilterText(song.Author);

                if ((searchOptions & SearchOptionFlags.Body) > 0)
                    searchText += FilterText(song.Body);

                if ((searchOptions & SearchOptionFlags.Title) > 0)
                    searchText += FilterText(song.Title);

                if ((searchOptions & SearchOptionFlags.Reference) > 0)
                    searchText += FilterText(song.Reference);

            }

            return searchText.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0;
        }

        private string directoryPath;

        public string DirectoryPath
        {
            get { return directoryPath; }
            set {
                if (directoryPath != value)
                {
                    directoryPath = value;

                    if (Directory.Exists(value))
                    {
                        watch.Path = value;
                        watch.EnableRaisingEvents = true;
                    }
                    else
                        watch.EnableRaisingEvents = false;
                    
                    loadCacheWorker.RunWorkerAsync();
                }                
            }
        }

        private string searchText = "";

        public string SearchText
        {
            get { return searchText; }
            set
            {
                if (searchText != value)
                {
                    searchText = value;
                }
            }
        }

        private string pendingSearch= "";

        public string PendingSearch
        {
            get { return pendingSearch; }
            set { pendingSearch = value; }
        }
	

        private List<SongLyrics> searchResults = new List<SongLyrics>();

        public List<SongLyrics> Results
        {
            get { return searchResults; }
            set { searchResults = value; }
        }

        private BindingList<string> searchLabels = new BindingList<string>();

        public BindingList<string> SearchLabels
        {
            get { return searchLabels; }
        }
	


        public void Search(string searchText)
        {
            if (searchWorker.IsBusy)
            {
                PendingSearch = searchText;
            }
            else
            {
                OnStartSearch();
                searchWorker.RunWorkerAsync(searchText);                    
            }                
        }

        public SongLyrics CacheSong(string fileName, bool forceReload)
        {
            SongLyrics newSong;

            if (songCache.ContainsKey(fileName))
            {
                newSong = songCache[fileName];

                if (forceReload) LoadSong(fileName, newSong);
            }
            else
            {
                newSong = new SongLyrics();

                LoadSong(fileName,newSong);

                //Cache the lyrics so the next time we do not need to load them from the file.
                lock(songCache) songCache[fileName] = newSong;
            }

            return newSong;
        }

        public void LoadSong(string fileName, SongLyrics newSong)
        {
            XmlDocument songFile = null;
            XmlNode songXml;

            try
            {

                int attempts = 6;
                while (attempts > 0)
                {
                    try
                    {
                        FileStream songStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
                        songFile = new XmlDocument();
                        songFile.Load(songStream);
                        break;
                    }
                    catch (IOException)
                    {
                        System.Threading.Thread.Sleep(1000);
                        attempts--;
                    }
                }

                if (songFile != null)
                {
                    songXml = songFile.SelectSingleNode("Song");
                    if (songXml != null)
                    {
                        newSong.Load(songXml);
                        newSong.FileName = fileName;
                    }
                }
            }
            catch (XmlException ex)
            {
                string msg;

                msg = string.Format("The song '{0}' appears to be unreadable or corrupted. You should remove or fix this file as it could effect the performance of Service Monkey. The error that was returned is '{1}'", fileName, ex.Message);
                MessageBox.Show(msg, "Load Song", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            catch (LoadLyricsException ex)
            {
                MessageBox.Show(ex.Message, "Load Song", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        #region IDisposable Members

        public void Dispose()
        {
            loadCacheWorker.Dispose();
        }

        #endregion
    }

    public class SearchResultArgs:EventArgs
    {
        public SearchResultArgs(string filename,SongLyrics lyrics)
        {
            FileName = filename;
            Lyrics = lyrics;
        }

        private string fileName;

        public string FileName
        {
            get { return fileName; }
            set { fileName = value; }
        }

        private SongLyrics lyrics;

        public SongLyrics Lyrics
        {
            get { return lyrics; }
            set { lyrics = value; }
        }
	
	
    }
}
